Skip to content

Detect and quarantine wedged vGPU VFs - #435

Open
yummybomb wants to merge 12 commits into
hypeship/vendor-vfio-vgpufrom
hypeship/vgpu-wedge-quarantine
Open

Detect and quarantine wedged vGPU VFs#435
yummybomb wants to merge 12 commits into
hypeship/vendor-vfio-vgpufrom
hypeship/vgpu-wedge-quarantine

Conversation

@yummybomb

@yummybomb yummybomb commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Killing the process holding a vendor VFIO VF during guest driver init (~8s window) silently corrupts the VF: every later boot on it fails while sysfs, NVML, and the vGPU plugin all look healthy. #321 removed hypeman's own kill triggers; this layer detects the wedges external kills (OOM, operator, deploy restarts) still cause, and takes the VF out of placement.

Detection — guest agent report, no lifecycle change. On a wedged VF the guest driver is present, trying, and failing: its kernel loops NVRM: ... RmInitAdapter failed! in the guest kernel log. The guest agent watches /dev/kmsg for that line and reports it as a HYPEMAN-GPU-INIT-FAILED marker — the same guest→host channel as the existing HYPEMAN-* markers, landing in the instance's logs/app.log. A new VGPUSentinelController tails that file for every instance holding a vendor VFIO assignment and convicts on the marker; the full marker shape is required since a bare token could appear in echoed exec command lines. The agent only watches when an NVIDIA PCI function is present, throttles re-emission to one per 30s, and matches the full kernel-line shape, never the driver-build-specific (0x22:0x65:884) tuple. A no-driver image never produces the kernel line and can never be convicted. An image that skips the guest agent does not report — v1 accepts that coverage bound; the raw kernel line still reaches app.log for manual diagnosis.

Quarantine. Convictions persist to <data-dir>/gpu/vf-health.json (keyed by VF address, survives restarts) under the placement lock. One wedge produces one record no matter how many victim boots or controller restarts report it. The store fails closed on an unreadable state file: mutations are refused, and vGPU placement and advertised availability are disabled (with load retried on each attempt) rather than treating the empty in-memory set as healthy and returning quarantined VFs to rotation. A conviction whose persist fails is rolled back from memory so the next report retries it instead of reading as a repeat. Start archives the previous boot's serial log before persisting a new assignment (fatal on failure for GPU instances), so a scan can never replay the prior boot's report against a freshly assigned VF. There is no rate limit on convictions: a systemic non-wedge init failure (e.g. a driver-mismatch rollout) emits the same line on every VF and would quarantine the whole host, so such changes are validated on a test host first and the convictions counter is the alerting signal if one gets through.

Placement.

  • Quarantined VFs are excluded from selection and from advertised profile availability. Healthy siblings on the same card stay available.
  • Cards are ordered by quarantined-VF count before load: a card with a wedged VF becomes overflow-only, so it drains toward the SR-IOV recovery cycle instead of staying warm.
  • The final VF pick within the chosen card is randomized: the previous lowest-address tiebreak routed every first create on an idle host to the same VF, making one wedged VF look like a total GPU outage.

Observability. Conviction logs at error level; hypeman_instances_vgpu_sentinel_convictions_total, the hypeman_instances_vgpu_quarantined_vfs gauge, and hypeman_instances_vgpu_vf_health_store_unavailable (1 while the persisted state file cannot be loaded and placement is failing closed). The controller idles on hosts without the vendor VFIO framework.

GPU.md documents the new behavior and the manual recovery flow (DCGM quiesce → SR-IOV cycle → restart services → verification boot → clear the VF's entry in vf-health.json → restart hypeman immediately).

Out of scope (follow-up layer)

An operator force-cycle endpoint (DCGM quiesce → sriov-manage -d/-e → verification boot → clear quarantine). Until then recovery stays the documented manual runbook.

Testing

  • Unit tests: kmsg record parsing/matching in the guest agent; marker pattern (incl. echoed-command and raw-kernel-line negative cases); tail behavior across partial lines, oversized lines, log archival, and new assignments; conviction retry on persist failure; conviction bursts across instances; kernel-facility-only kmsg matching (userspace /dev/kmsg writes cannot forge the report); repeated marker emission so a printk write-split cannot lose a report; already-quarantined rescans after controller restarts; quarantine store persistence, one-record-per-wedge idempotence, and load-failure refusal; placement exclusion / card bias / tiebreak; availability accounting.
  • go test -race ./lib/devices ./lib/paths ./lib/providers ./lib/system/guest_agent and the vGPU-related lib/instances tests pass locally; go vet clean. The full lib/instances suite fails only on environment-dependent VM/network tests, identically on the unmodified base.
  • The detection signal (kernel-line timing, serial-console delivery into app.log) and the recovery sequence were validated on real L40S hardware by deliberately wedging VFs; the guest-agent watcher and the controller have not yet run against a live wedge end-to-end — that run should happen on the dev GPU host before this merges.

Note

High Risk
Changes vGPU placement, advertised GPU capacity, and fail-closed behavior when VF health state is unreadable, so a bad store or a systemic init-failure signal can take a host out of GPU service. Guest-emitted markers can also shrink capacity (never mutate the instance).

Overview
Automatically detects wedged NVIDIA vGPU VFs (guest RmInitAdapter timeout with a healthy-looking host stack) and takes them out of placement until an operator SR-IOV-cycles the parent GPU.

The guest agent watches /dev/kmsg on NVIDIA guests and emits a HYPEMAN-GPU-INIT-FAILED marker. A new VGPUSentinelController tails vendor-VFIO instance app.logs, quarantines the assigned VF in gpu/vf-health.json (fail-closed if the file is unreadable), and excludes those VFs from selection and advertised capacity. Parent GPUs with quarantined VFs become overflow-only; VF pick within a card is randomized so one undetected wedge no longer looks like a total GPU outage.

Start now archives the previous boot log before a new vGPU assignment (fatal for GPU instances) so a prior marker cannot convict a fresh VF. Metrics cover convictions, quarantine count, and health-store unavailability. Recovery remains a documented manual DCGM quiesce + SR-IOV cycle + clear-quarantine restart.

Reviewed by Cursor Bugbot for commit 44b8a12. Bugbot is set up for automated code reviews on this repo. Configure here.

@yummybomb
yummybomb force-pushed the hypeship/vgpu-wedge-quarantine branch from d312338 to 7d54fb9 Compare August 20, 2026 19:12
@yummybomb
yummybomb marked this pull request as ready for review August 20, 2026 19:33
Comment thread lib/devices/vf_health.go Outdated
@yummybomb
yummybomb force-pushed the hypeship/vgpu-wedge-quarantine branch from 7d54fb9 to 9a90223 Compare August 20, 2026 20:17
@yummybomb
yummybomb force-pushed the hypeship/vgpu-wedge-quarantine branch from 9a90223 to 0ff16d1 Compare August 20, 2026 21:57

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0ff16d1. Configure here.

Comment thread lib/devices/vf_health.go
@yummybomb
yummybomb force-pushed the hypeship/vgpu-wedge-quarantine branch from 0ff16d1 to 3db5546 Compare August 20, 2026 22:04
Detection moves from host-side matching of the raw NVRM kernel line to an
explicit guest-to-host report: the guest agent watches /dev/kmsg for the
driver's RmInitAdapter failure and emits a HYPEMAN-GPU-INIT-FAILED marker
over the established sentinel-marker channel, which the controller convicts
on. The raw kernel line is no longer matched, so detection now requires the
guest agent; images without it do not report.

Quarantine hardening in the same pass:

- The conviction brake pauses instead of drops: a suppressed conviction
  leaves the tail open and the agent's re-emission retries it once the
  window clears.
- A match on an already-quarantined VF is not a new conviction: no brake
  accounting, no metric, no wedge-count inflation on controller restarts.
- Tails reset when the instance acquires a new assignment, so a finished
  tail from a previous boot or VF cannot suppress scanning the next one.
- A vf-health state file that fails to load refuses mutations (and retries
  the load) instead of letting the next conviction clobber every previously
  persisted quarantine.
- Oversized unterminated log lines are skipped instead of re-buffered on
  every scan.
- The controller idles on hosts without the vendor VFIO framework.

GPU.md: DCGM quiesce is now an ordered step of the recovery sequence, and
clearing vf-health.json documents the immediate-restart requirement.
Review fixes on the quarantine layer:

- start now archives the previous boot's serial log before persisting the
  new vGPU assignment. The sentinel keys its tail on the assignment epoch,
  so the old ordering let a scan replay the previous boot's wedge report
  against the freshly assigned VF. A failed archive is fatal for GPU
  instances instead of a warning.
- placement and profile availability refuse to run when the VF health
  state file exists but cannot be loaded, instead of treating the empty
  in-memory set as healthy and returning every quarantined VF to
  rotation. The load is retried on each attempt, so a repaired file
  self-heals.
- a conviction whose persist fails is rolled back from memory: keeping it
  made the next report look like a repeat conviction, ending retries with
  nothing on disk.
- the marker pattern requires the full shape through the quoted NVRM
  payload; a truncated or payload-less marker echoed by an exec command
  no longer matches.
- unreadable instance metadata in the sentinel target listing logs a
  warning instead of silently shrinking detection coverage.
- GPU.md recovery runbook unwound a circularity: placement excludes
  quarantined VFs and there is no VF-pin API, so the entry is cleared
  before the verification boot; the sentinel re-quarantines automatically
  if the cycle did not cure the VF.
The /dev/kmsg record priority encodes facility*8+level. Kernel printk is
always facility 0 and the kernel assigns userspace writers LOG_USER or
higher (a facility-0 prefix is coerced to LOG_USER, verified on a live
6.12 kernel), so requiring facility 0 makes in-guest forgery of the
report impossible, matching the intended kernel-records-only semantics.

Also retry a failed /dev/kmsg open instead of permanently disabling the
watcher for the guest's lifetime.
The scan buffered each complete line whole, so guest console output
could make the controller allocate line-sized buffers every pass; the
64KB cap only kept an unterminated tail from being re-read, and once
such a line was skipped its late-arriving tail was parsed as a fresh
line. Read through a fixed-size buffer instead: a line that overflows
it cannot be a marker, so it is discarded — across scans if its newline
has not arrived — without ever being held in memory, and its tail can
no longer replay a marker. Rotation resets the skip state with the
offset.
When the persisted state file fails to load, quarantine mutations are
refused and vGPU placement fails closed, but the quarantined-VFs gauge
reads zero from the empty in-memory set — exactly when quarantines
exist and are unreadable. Export the load-failure state as its own
gauge so the condition is alertable.
@yummybomb
yummybomb force-pushed the hypeship/vgpu-wedge-quarantine branch from 3db5546 to b35501a Compare August 21, 2026 15:13
The serial console is a shared byte stream: kernel printk bypasses the
tty buffer and can land mid-marker, and on a wedged VF the kernel is
emitting NVRM errors exactly when the agent reports. A corrupted copy
does not match the host's full-shape scan (deliberately, so echoed
commands cannot convict), which delayed the report to the next 30s
re-emission. Emit each report as three identical lines sharing one ts;
the host convicts on the first intact copy and ignores the rest.
Auto-conviction no longer pauses on a burst. Systemic non-wedge init
failures (e.g. a driver-mismatch image rollout) are expected to be
caught on a test host before reaching production, and the convictions
counter remains the alerting signal if one gets through; the brake was
extra state and logic guarding against a case the rollout process
already covers. Quarantine still only removes capacity and never
touches instances, and the store's fail-closed load handling is
unchanged.
Each /dev/kmsg read returns exactly one record and fails with EINVAL —
without consuming the record — when the buffer is smaller. Records run
up to CONSOLE_EXT_LOG_MAX (8 KiB), so bufio's default 4 KiB buffer
wedged the watcher on the first oversized record: every reopen replayed
the ring into the same record, silently losing all detection behind it.
Size the buffer to the kernel's record bound and log non-EPIPE scan
errors so a wedge is visible instead of silent.
A quarantine is only real once it is on disk, but the persist renamed
without syncing the file or directory, so a host crash right after a
conviction could silently drop it. Sync the temp file before the rename
and the directory after, and deduplicate the sorted record listing.
The matched line is guest-controlled console bytes up to the 64 KiB
line cap; logging and persisting it verbatim put up to that much guest
output in error logs and vf-health.json. Keep just the marker match.
ClearVFQuarantine had no callers — the runbook documents editing
vf-health.json directly — and WedgeCount was always 1. QuarantineVF's
record return and IsVFQuarantined only served tests, and the sentinel's
isQuarantined pre-check duplicated what QuarantineVF already reports
via existed. The hostFramework indirection wrapped a single
DiscoverVGPU call nothing injected.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant